package com.ease.util.proxies {

   import com.ease.util.Diagnostics;
   import com.ease.util.MixedUtil;
   import com.ease.util.StringUtils;
   import com.ease.util.logging.LogManager;

   import flash.events.TimerEvent;
   import flash.utils.Timer;

   import mx.logging.ILogger;
   import mx.messaging.events.ChannelFaultEvent;
   import mx.rpc.AbstractOperation;
   import mx.rpc.AsyncToken;
   import mx.rpc.Fault;
   import mx.rpc.IResponder;
   import mx.rpc.events.FaultEvent;
   import mx.rpc.events.ResultEvent;

   /**
    * Invokes operation representing an individual method on a service. Also provides the
    * utility of retrying a failed request incase the failure was due to a network failure.
    */
   internal class OperationInvoker implements IResponder {
      private static var _logger:ILogger = LogManager.getLogger(OperationInvoker);

      private static const DEFAULT_RETRY_COUNT:int = 10;
      private static const DEFAULT_RETRY_PERIOD:Number = 500; //In ms.
      // Error code and descriptions
      private static const NETCONNECTION_CALL_FAILED:String = "NetConnection.Call.Failed";
      private static const HTTP_FAILED:String = "HTTP: Failed";

      /**
       * Number of times to retry failed operations.
       */
      public var retryCount:int = DEFAULT_RETRY_COUNT;

      /**
       * Time between detection of failure and next retry of an operation specified in ms.
       */
      public var retryPeriod:Number = DEFAULT_RETRY_PERIOD;

      private var _timer:Timer;
      private var _currentRetryCount:int = 0;
      private var _retryTokens:Array;

      /**
       * Will invoke the operation and return the result or fault to the supplied callback.
       *
       * @param operation
       *    Operation of service to be invoked.
       *
       * @param arguments
       *    Arguments of the operation.
       *
       * @param callback
       *    The response of invocation is provided to the callback.
       *
       *    <code>function(event:Event, callContext:Object)</code>
       *    Expect event to be of type ResultEvent or FaultEvent.
       *
       * @param callContext
       *   Free form context which will be returned to the callback as is.
       */
      public function invoke(
         operation:AbstractOperation,
         arguments:Array,
         callback:Function,
         callContext:Object):void {
         operation.arguments = arguments;
         var token:AsyncToken = operation.send();
         token.addResponder(this);
         token.callback = callback;
         token.callContext = callContext;
         token.operation = operation;
      }

      /**
       * @inheritDoc
       */
      public function result(data:Object):void {
         var event:ResultEvent = data as ResultEvent;
         Diagnostics.assert(event != null, "Expecting data to be a ResultEvent.");
         if (event == null) {
            return;
         }
         // Since this is a successful response replay all requests
         replayFailedOperations();
         var token:AsyncToken = event.token;
         resultResponseForRequest(event, token);
      }

      /**
       * @inheritDoc
       */
      public function fault(info:Object):void {
         var event:FaultEvent = info as FaultEvent;
         Diagnostics.assert(event != null, "Expecting info to be a FaultEvent.");
         if (event == null) {
            return;
         }
         var token:AsyncToken = event.token;
         if (retryOnFault(event, token)) {
            return;
         }
         faultResponseForRequest(event, token);
      }

      /**
       * Provides a result response to the contained BaseProxy.
       */
      private function resultResponseForRequest(event:ResultEvent, token:AsyncToken):void {
         var callback:Function = token.callback;
         var callContext:Object = token.callContext;
         if (callback != null) {
            callback(event, callContext);
         }
      }

      /**
       * Provides a fault response to the contained BaseProxy.
       */
      private function faultResponseForRequest(event:FaultEvent, token:AsyncToken):void {
         var callback:Function = token.callback;
         var callContext:Object = token.callContext;
         if (callback != null) {
            callback(event, callContext);
         }
      }

      /**
       * Will replay the operation contained in the token on the tick of a timer.
       */
      private function replayOperationOnTimer(token:AsyncToken):void {
         if (_timer == null) {
            _timer = new Timer(retryPeriod, 1);
            _timer.addEventListener(TimerEvent.TIMER, onTimerEvent);
         }
         if (_retryTokens == null) {
            _retryTokens = [];
         }
         // As an extra precaution remove responder from this token.
         MixedUtil.removeItemFromArray(this, token.responders);
         // Adding to back of the retry token queue so that it can be used to replay
         _logger.debug("Adding token to retry list for a replay of the operation.");
         _retryTokens.push(token);
         // If the timer is already running from a previous replay attempt then just queue
         // up the token for replay
         if (!_timer.running) {
            _timer.reset();
            _timer.start();
         }
      }

      /**
       * Picks the first token from the tokens list and replays the contained operation.
       */
      private function onTimerEvent(event:TimerEvent):void {
         // stop the timer to prevent any more events until the next fault is detected.
         _timer.stop();
         // Nothing to retry
         if (_retryTokens.length < 1) {
            return;
         }
         // Pick up the first token in the retry list. It really does not matter which
         // request is replayed so no single replay token is maintained.
         var token:AsyncToken = _retryTokens.shift();
         // increment retry counter
         ++_currentRetryCount;
         replayOperation(token);
      }

      /**
       * Replays all the failed operations.
       */
      private function replayFailedOperations():void {
         if (_currentRetryCount > 0) {
            _logger.info("Seems like the client recovered from a network failure.");
         }
         _currentRetryCount = 0;
         clearTimer();
         if (MixedUtil.isNullOrEmpty(_retryTokens)) {
            return;
         }
         // Replay all request in a tight loop.
         for each (var token:AsyncToken in _retryTokens) {
            replayOperation(token);
         }
         _retryTokens = null;
      }

      /**
       * Replays the operation contained in the supplied token.
       */
      private function replayOperation(token:AsyncToken):void {
         var operation:AbstractOperation = token.operation;
         var callback:Function = token.callback;
         var callContext:Object = token.callContext;
         invoke(operation, operation.arguments as Array, callback, callContext);
      }

      /**
       * Delivers a fault response to all the failed operations. Since this  method should
       * be called only on failure to reestablish connection it will not reset
       * _currentRetryCount. This way all successive replayable faults will be immediately
       * returned as errors until the next succesful result or non-replayable fault.
       */
      private function faultResponseToFailedOperations(event:FaultEvent):void {
         clearTimer();
         if (MixedUtil.isNullOrEmpty(_retryTokens)) {
            return;
         }
         // Replay all request in a tight loop.
         for each (var token:AsyncToken in _retryTokens) {
            faultResponseForRequest(event, token);
         }
         _retryTokens = null;
      }

      /**
       * Will retry the operation if event indicates a failure warranting a retry. In case
       * event does not provide such an indication will replay all the operations that
       * are currently saved as failures.
       *
       * Note : Will only retry an operation on a potential network failure.
       *
       * @return
       *    Returns true if the result should be hidden from the caller.
       */
      private function retryOnFault(event:FaultEvent, token:AsyncToken):Boolean {
         if (event == null || event.fault == null) {
            return false;
         }
         var retriableFailure:Boolean = isPotentialNetworkFailure(event.fault);
         if (retriableFailure && event.fault != null) {
            _logger.error("A failure which could be fixed by a retry is detected - " +
               event.fault.message);
         } else {
            // Is not a network failure so replay all the failed requests as this could mean
            // that the network is back again.
            replayFailedOperations();
            return false;
         }
         if (_currentRetryCount < this.retryCount) {
            replayOperationOnTimer(token);
            return true;
         }
         _logger.error("Unable to recover from failure.");
         // Is a network failure and max retries have been attempted so return errors.
         // Since this is the same error ok to reuse the FaultEvent.
         faultResponseToFailedOperations(event);
         return false;
      }

      /**
       * Checks if fault indicates a possible network failure.
       */
      private function isPotentialNetworkFailure(fault:Fault):Boolean {
         var channelFaultEv:ChannelFaultEvent = fault.rootCause as ChannelFaultEvent;
         if (channelFaultEv == null || channelFaultEv.rootCause == null) {
            return false;
         }
         // This rootcause implies that the server is unreachable because of one of the
         // following issues :
         // - Network failure
         // - Server shutdown
         // Note : The description in case the messagebroker goes down would contain
         // an error code like HTTP 400.
         return channelFaultEv.rootCause.code == NETCONNECTION_CALL_FAILED &&
            channelFaultEv.rootCause.description == HTTP_FAILED;
      }

      /**
       * Stop timer, remove event listeners and null out reference.
       */
      private function clearTimer():void {
         if (_timer == null) {
            return;
         }
         _timer.stop();
         _timer.removeEventListener(TimerEvent.TIMER, onTimerEvent);
         _timer = null;
      }
   }
}